package com.cuteserver.client;

/*
This file is part of Cute-Client.

Cute-Client is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Cute-Client is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Cute-Client.  If not, see <http://www.gnu.org/licenses/>.
*/

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;

import org.json.JSONArray;
import org.json.JSONException;

/**
 * CSSession object is the representation of a connection with a session
 * on the cute-server side. 
 * 
 * @author Aurelien Deharbe
 * @version 1.0
 */
public class CSSession {

	/**
	 * The local port of connexion with session
	 */
	private int port;
	
	/**
	 * The socket connected with the session
	 */
	private Socket socket;
	
	/**
	 * The listening socket
	 */
	private ServerSocket server;
	
	/**
	 * The writer on socket connected with the session
	 */
	private PrintWriter writer;
	
	/**
	 * The reader on socket connected with the session
	 */
	private BufferedReader reader;
	
	/**
	 * The callbacks table
	 */
	private HashMap<String, CSICallback> callbacks;
	
	/**
	 * Construct and connect the session object on the first free local port
	 * from 5001 to 8000
	 * @throws CSException If there is connection problem,
	 * or if there is no available port from 5001 to 8000
	 */
	CSSession() throws CSException
	{
		this.callbacks = new HashMap<String, CSICallback>();
		this.port = 5001;
		boolean cont = true;
		do {
			try {
				this.server = new ServerSocket(this.port);
				cont = false;
			} catch(IOException e) {
				port++;
			}
		} while (cont || this.port > 8000);
		if (cont) throw new CSException(CSException.CONNEXION);
	}
	
	/**
	 * Send a command to the cute-server session and return the result as an Object
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public Object qs(String cmd) throws CSException
	{
		cmd = cmd + "\n";
		String ret;
		try {
			writer.write(cmd);
			writer.flush();
			ret = reader.readLine();
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
		try {
			JSONArray ja = new JSONArray(ret);
			Object result = ja.get(ja.length()-1);
			return result;
		} catch (JSONException e) {
			throw new CSException(CSException.JSON_DECODING);
		}
	}
	
	/**
	 * Send a command to the cute-server session and return the result as a boolean
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public boolean qsb(String cmd) throws CSException
	{
		Boolean ret = (Boolean)this.qs(cmd);
		return ret.booleanValue();
	}
	/**
	 * Send a command to the cute-server session and return the result as a double
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public double qsd(String cmd) throws CSException
	{
		Double ret = (Double)this.qs(cmd);
		return ret.doubleValue();
	}
	
	/**
	 * Send a command to the cute-server session and return the result as a float
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public float qsf(String cmd) throws CSException
	{
		Float ret = (Float)this.qs(cmd);
		return ret.floatValue();
	}
	
	/**
	 * Send a command to the cute-server session and return the result as an int
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public int qsi(String cmd) throws CSException
	{
		Integer ret = (Integer)this.qs(cmd);
		return ret.intValue();
	}
	
	/**
	 * Send a command to the cute-server session and return the result as a long
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public long qsl(String cmd) throws CSException
	{
		Long ret = (Long)this.qs(cmd);
		return ret.longValue();
	}
	
	/**
	 * Send a command to the cute-server session and return the result as a String
	 * @param cmd The command to be executed on the session
	 * @return The command response
	 * @throws CSException If there is a connection problem,
	 * or if the command contains error
	 */
	public String qss(String cmd) throws CSException
	{
		return (String)this.qs(cmd);
	}
	
	/**
	 * Register a callback on a distant event
	 * @param obj Name of the event handler
	 * @param evt Name of the QT event
	 * @param clb QSICallback object
	 * @throws CSException If there is a connection problem,
	 * or if the arguments are not recognized
	 */
	public void registerCallback(String obj, String evt, CSICallback clb)
	throws CSException
	{
		this.registerCallback(obj, evt, clb, "");
	}
	
	/**
	 * Register a callback on distant data changing
	 * @param data Name of the data
	 * @param clb QSICallback object
	 * @throws CSException If there is a connection problem,
	 * or if the arguments are not recognized
	 */
	public void registerCallback(String data, CSICallback clb)
	throws CSException
	{
		this.registerCallback(data, "qsdatachanged", clb, "");
	}
	
	/**
	 * Register a callback on distant typed event
	 * @param obj Name of the event handler
	 * @param evt Name of the QT event
	 * @param clb QSICallback object
	 * @param tps Arguments types list (separated by ',')
	 * @throws CSException If there is a connection problem,
	 * or if the arguments are not recognized
	 */
	public void registerCallback(String obj, String evt, CSICallback clb, String tps)
	throws CSException
	{
	    String cmd = "scripts.subscribe('" + obj;
	    cmd = cmd + "','" + evt + "','" + tps + "','#me#')\n";
		String ret;
		try {
			writer.write(cmd);
			writer.flush();
			ret = reader.readLine();
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
		try {
			JSONArray ja = new JSONArray(ret);
			for (int i = 0 ; i < ja.length() ; i++) {
				callbacks.put(ja.getString(i) + "." + evt, clb);
			}
		} catch (JSONException e) {
			throw new CSException(CSException.JSON_DECODING);
		}
	}
	
	/**
	 * Enter in the cute-client main loop
	 * @throws CSException If there is a connection problem
	 */
	public void mainloop() throws CSException
	{
		String ret;
		while (true) {
			try {
				ret = reader.readLine();
			} catch(IOException e) {
				return;
			}
			if (ret == null || ret.equals("")) {
				return;
			}
			try {
				JSONArray ja = new JSONArray(ret);
				String obj = ja.getString(0);
				String evt = obj + "." + ja.getString(1);
				if (! callbacks.containsKey(evt)) {
					continue;
				}
				Object[] args = new Object[ja.length() - 2];
				for (int i = 2 ; i < ja.length() ; i++) {
					args[i-2] = ja.get(i);
				}
				callbacks.get(evt).callback(obj, args);
			} catch (JSONException e) {
				throw new CSException(CSException.JSON_DECODING);
			}
		}
	}
	
	/**
	 * Put a data on the cute-server session
	 * @param name Name of the data
	 * @param value Value of the data
	 * @throws CSException If there is a connection problem
	 */
	public void putData(String name, Object value)
	throws CSException
	{
		JSONArray ja = new JSONArray();
		ja.put(value);
		String cmd = "json.loads('" + ja.toString() + "')[0]";
		cmd = cmd + " -> " + name.substring(1, name.length() - 1);
		cmd = cmd + "\n";
		try {
			writer.write(cmd);
			writer.flush();
			reader.readLine();
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
	}
	
	/**
	 * Put a tab data on the cute-server session
	 * @param name Name of the data
	 * @param value Value of the tab data
	 * @throws CSException If there is a connection problem 
	 */
	public void putDataTab(String name, Object[] value)
	throws CSException
	{
		try {
			JSONArray ja = new JSONArray(value);
			String cmd = "json.loads('" + ja.toString() + "')";
			cmd = cmd + " -> " + name.substring(1, name.length() - 1);
			cmd = cmd + "\n";
			writer.write(cmd);
			writer.flush();
			reader.readLine();
		} catch(JSONException e) {
			throw new CSException(CSException.JSON_DECODING);
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
	}
	
	/**
	 * Retrieve the value of a distant data
	 * @param name Name of the data
	 * @return Data retrieved
	 * @throws CSException If there is a connection problem
	 */
	public Object getData(String name)
	throws CSException
	{
		try {
			String cmd = name + "\n";
			writer.write(cmd);
			writer.flush();
			String ret = reader.readLine();
			JSONArray ja = new JSONArray(ret);
			Object result = ja.get(ja.length()-1);
			return result;
		} catch(JSONException e) {
			throw new CSException(CSException.JSON_DECODING);
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
	}
	
	/**
	 * Retrieve the value of a distant tab data
	 * @param name Name of the data
	 * @return Tab data retrieved
	 * @throws CSException If there is a connection problem
	 */
	public Object[] getDataTab(String name)
	throws CSException
	{
		try {
			String cmd = name + "\n";
			writer.write(cmd);
			writer.flush();
			String ret = reader.readLine();
			JSONArray ja = new JSONArray(ret);
			JSONArray jai = ja.optJSONArray(0);
			Object[] result = new Object[jai.length()];
			for (int i = 0 ; i < jai.length(); i++) {
				result[i] = jai.get(i);
			}
			return result;
		} catch(JSONException e) {
			throw new CSException(CSException.JSON_DECODING);
		} catch(IOException e) {
			throw new CSException(CSException.CONNEXION);
		}
	}
	
	/**
	 * Get the local port of socket connected with the cute-server session
	 * @return The local port
	 */
	int getPort()
	{
		return port;
	}
	
	/**
	 * Wait for the entering connection from the cute-server session
	 * @throws IOException If there is a connection problem
	 */
	void accept() throws IOException
	{
		socket = server.accept();
		server.close();
		
		InputStream is = socket.getInputStream();
		InputStreamReader isr = new InputStreamReader(is);
		reader = new BufferedReader(isr);
		
		OutputStream os = socket.getOutputStream();
		OutputStreamWriter osw = new OutputStreamWriter(os);
		BufferedWriter bw = new BufferedWriter(osw);
		writer = new PrintWriter(bw);
	}
}
